<?php

declare(strict_types=1);

namespace openapi;

use openapi\oas\OAS;
use openapi\oas\Service;
use openapi\oas\path\Request;
use openapi\oas\path\Parameter;
use openapi\oas\path\Schema;

class ApiDoc
{
	/**
	 * 存放所有包含的php类
	 * 由于内部需要递归目录，必须用数组暂存合并
	 * @var array
	 */
	public $classes = [];

	/**
	 * @var string TP定义的应用命名空间
	 */
	public $app_namespace = 'app';

	/**
	 * @param string $dirs 目录路径，可以是数组或字符串
	 * @param string $app_namespace TP定义的应用命名空间，在TP6中是app，TP5中改为application
	 */
	public function __construct($dirs = null, $app_namespace = 'app')
	{
		$this->app_namespace = $app_namespace;

		if (is_string($dirs)) $dirs = [$dirs];
		else if (is_null($dirs)) $dirs = [];

		foreach ($dirs as $dir) {
			$this->parseDirToClass($dir);
		}
	}

	/**
	 * 扫描一个目录
	 * 递归其中的子目录，把所有php文件转为命名空间类存入classes
	 * @param string $dir 目录路径
	 * @return $this
	 */
	public function parseDirToClass(string $dir)
	{
		$dirNamespace = $this->app_namespace;

		$dir = str_replace('/', DIRECTORY_SEPARATOR, $dir);
		$reader = opendir($dir);
		$arrays = [];
		while ($filename = readdir($reader)) {
			if ($filename == '.' || $filename == '..') continue;
			$full_path = $dir . DIRECTORY_SEPARATOR . $filename;
			if (is_dir($full_path)) {
				$this->parseDirToClass($full_path);
			} else {
				// 有可能出现完整路径中也携带 $dirNamespace 的情况，例如  /www/wwwroot/weapp.xxx.com/app 这里用app匹配的话就不对了，所以要加个 '/'
				$rootSearchStr = $dirNamespace . DIRECTORY_SEPARATOR;

				$search = strstr($full_path, $rootSearchStr);
				if ($search === false) throw new \Exception('控制器所在路径中未能匹配到[' . ($rootSearchStr) . ']，请检查$dirNamespace参数');
				$namespace = substr($search, strlen($rootSearchStr));

				$namespace = str_replace('/', '\\', substr($namespace, 0, strrpos($namespace, DIRECTORY_SEPARATOR)));
				$classname = $dirNamespace . '\\' . $namespace . '\\' . str_replace('.php', '', $filename);
				$arrays[] = $classname;
			}
		}
		$this->classes = array_merge($this->classes, $arrays);
		return $this;
	}

	/**
	 * 把所有classes转为api输出
	 */
	public function parseClassToApi(): OAS
	{
		// 处理operationId
		$operationIdList = [];
		$apis = [];
		foreach ($this->classes as $class) {
			$ref = new \ReflectionClass($class);
			$classComment = $this->parseComment($ref->getDocComment());

			// 类的分组名（API组名）
			$classTagName = '';
			foreach ($classComment as $comment) {
				if ($comment[0] == 'api') {
					$classTagName = $comment[1];
					break;
				}
			}
			if (!$classTagName) continue;

			// 所有api
			$methods = $ref->getMethods();
			foreach ($methods as $method) {

				$comments = $this->parseComment($method->getDocComment());
				$commentApi = null;
				// 方法必须包含一个 @api 注释，否则过滤
				foreach ($comments as $comment) {
					if ($comment[0] == 'api') {
						$commentApi = $comment;
						break;
					}
				}
				if (is_null($commentApi)) continue;

				// 这里得到每一个合格的api方法， @commentApi 就是这一条@api注释
				$explodsClass = explode('\\', $class);

				$path = '';
				foreach ($explodsClass as $i => $part) {
					if (in_array($part, [$this->app_namespace, 'controller'])) continue;
					$path .= $part . '/';
				}
				$path .= $method->name;

				// 默认请求方法
				$requestMethod = 'POST';

				$request = new Request;
				$request->tags = [$classTagName];

				// 处理operationId，规则： {类名}_{方法名}
				$operationId = $ref->getShortName() . '_' . $method->name;
				if (in_array($operationId, $operationIdList)) {
					$operationId .= '2';
				}
				$request->operationId = $operationId;
				$operationIdList[] = $operationId;

				// 支持两种写法
				// @api POST 接口名称
				// @api 接口名称
				$commentApi = array_filter($commentApi, fn($i) => !!$i); // 注意在空格缩进和tab缩进有区别，空格缩进会多一个''项，应该过滤

				if (count($commentApi) > 2) {
					$request->summary = $commentApi[2];
					$requestMethod = strtoupper($commentApi[1]);
				} else {
					$request->summary = $commentApi[1];
				}

				$params = [];
				// 遍历每一行注释@
				foreach ($comments as $comment) {
					// api描述
					if ($comment[0] == 'desc') {
						$request->description = implode(' ', array_slice($comment, 1));
					}
					// 每个参数
					if ($comment[0] == 'param') {

						$param = new Parameter;
						$param->name = substr($comment[2], 1);
						$param->description = $comment[3] ?? '';
						$param->in = 'query';
						$param->required = !isset($comment[4]);

						$schema = new Schema;

						// 处理参数类型，把php里的int等转换为OAS的类型
						$phpType = $comment[1] ?? 'string';
						$exampleStr = $comment[4] ?? null;

						if (in_array($phpType, ['int', 'integer'])) {
							$schema->type = 'integer';
							$schema->example = is_null($exampleStr) ? null : (int)$exampleStr;
						} elseif (in_array($phpType, ['float', 'double'])) {
							$schema->type = 'number';
							$schema->example = is_null($exampleStr) ? null : (float)$exampleStr;
						} elseif ($phpType == 'string') {
							$schema->type = 'string';
							$schema->example = $exampleStr;
						} elseif (in_array($phpType, ['bool', 'boolean'])) {
							$schema->type = 'boolean';
							$schema->example = is_null($exampleStr) ? null : (strtolower($exampleStr) == 'true');
						} elseif (in_array($phpType, ['array', 'object'])) {
							$schema->type = $phpType;
							$schema->example = is_null($exampleStr) ? null : json_decode($exampleStr, true);
						} else {
							// 其他类型直接使用
							$schema->type = $phpType;
						}

						if ($exampleStr === 'null') {
							$schema->example = null;
						}

						$param->schema = $schema;
						$params[] = $param;
					}
				}

				// GET
				$request->parameters = $params;

				// POST 把参数也放到body中
				if ($requestMethod == 'POST') {
					$postProperties = [];
					foreach ($params as $p) {
						$postProperties[$p->name] = [
							'type' => $p->schema->type,
							'description' => $p->description
						];
					}
					$request->requestBody = [
						'required' => true,
						'content' => [
							'application/json' => [
								'schema' => [
									'type' => 'object',
									'properties' => $postProperties
								]
							]
						]
					];
				}

				// 处理请求方法
				$apis[$path] = [$requestMethod => $request];
			}
		}

		$oas = new OAS();
		$oas->paths = $apis;

		return $oas;
	}

	/**
	 * 解析commit
	 * 把一个@开头的注释行按空格分割成数组
	 * @param string $comment
	 */
	protected function parseComment($comment)
	{
		if (!is_string($comment)) return [];

		$line_array = preg_split('/\r\n|\r|\n/', $comment);
		$result = [];
		foreach ($line_array as $line) {

			// 过滤无@开头的项
			if (strpos($line, '@') === false) continue;

			// 移除@之前的符号 [/ * @title 标题] ===> [title 标题]
			$res = preg_replace("/.+@/", '', $line);

			// 除去多个空格的情况 [param int id  商品ID] ===> [param int id 商品ID]
			$res = preg_replace("/\s+/", ' ', $res);

			// 按空格分隔 ['param', 'int', 'id', '商品ID'], 
			$result[] = explode(' ', $res);
		}
		return $result;
	}
}
